Choreographer可以认为是连接底层和应用层的中间人角色。对下,它负责注册并接收底层发送的Vsync信号;对上,负责在应用层协调下一帧的绘制、事件、动画过程。Choreographer配合SurfaceFlinger、Triple Buffer为Android系统提供稳定的帧率刷新环境。
Choreographer初始化
Vsync信号简介
VSYNC 信号可以认为是一种定时中断信号。为了配合主流屏幕60Hz的刷新频率,即16.6ms一次,系统也控制Vsync信号的周期为16.6ms。Vsync信号在应用层会唤醒Choreographer响应用户输入、APP绘制等操作,在合成层会唤醒SurfaceFlinger,将准备好的Surface进行合成操作。
Vsync信号主要由硬件HWC(Hardware Composer,硬件混合渲染器)生成。然后通过回调将事件发送到SurfaceFlinger和Choreographer。
1 | typedef void (*HWC2_PFN_VSYNC)(hwc2_callback_data_t callbackData, |
DispSync将硬件Vsync信号转换成Choreographer和SurfaceFlinger使用的VSYNC和SF_VSYNC信号。
Choreographer初始化调用链
通过AS Profiler观察方法调用链,Activity启动后,执行完ActivitityThread.performResumeActivity(),再执行WindowManagerImpl.addView(),在其内部会执行ViewRootImpl的初始化。Choreographer的初始化就是在ViewRootImpl的构造方法中执行的。
1 | android.view.ViewRootImpl.java |
Choreographer初始化过程
-> 初始化FrameHandler,绑定Looper,响应事件
-> 初始化FrameDisplayEventReceiver,与SurfaceFlinger建立联系,响应、调度Vsync
-> 初始化CallbackQueue
1 | android.view.Choreographer.java |
执行核心方法doFrame()和调度Vsync都是通过Handler实现。
1 | private final class FrameHandler extends Handler { |
FrameDisplayEventReceiver
Choreographer对Vsync信号的响应主要体现在FrameDisplayEventReceiver。这个类负责对Vsync信号注册、调度、响应。
FrameDisplayEventReceiver有三个主要方法:
- onVsync():响应Vsync信号
scheduleVsync():调度Vsync信号
run():执行doFrame(),doFrame()内部会做计算掉帧、响应Input、Animation、Traversal(measure/layout/draw)、Commit
响应Vsync
HWC触发Vsync信号后,在Java层会回调DisplayEventReceiver.onVsync(),该方法是空实现,本质上走到子类的FrameDisplayEventReceiver.onVsync()方法。
1 | private final class FrameDisplayEventReceiver extends DisplayEventReceiver |
本质上是通过FrameHandler发送了一个Callback为FrameDisplayEventReceiver自身的message,当Looper执行到该消息时会调用FrameDisplayEventReceiver.run()方法,该Looper一般是主线程的Looper。
其中mTimestampNanos记录了Vsync信号到来的时间点,后面计算掉帧时会用到。
run()
1 | @Override |
内部调用Choreographer.doFrame().
Choreographer处理每一帧
doFrame()是Choreographer处理每一帧操作的核心。
内部先进行掉帧计算,记录帧信息,然后执行INPUT(用户输入)、ANIMATION、TRAVERSAL(measure/layout/draw)、COMMIT等回调。
- INPUT:输入事件
- ANIMATION:动画
- TRAVERSAL:页面刷新,响应measure、layout、draw
- COMMIT:处理commit回调,修正最后一帧的时间戳
掉帧计算
掉帧计算的逻辑在下面这段代码,Vsync信号到来时记录一个开始时间,即onVsync()中的mTimestampNanos。在主线程MessageQueue中轮到doFrame()的message执行后(Vsync信号到来有可能不会立马处理,因为这中间主线程可能被其他任务占用),在doFrame()中记录一个结束时间,即下面代码中的startNanos。这两个时间差值就是Vsync信号的处理时延,即掉帧时间。
平时开发中大家有可能在Logcat看到下面这个log:”Skipped N frames! The application may be doing too much work on its main thread.” 掉帧超过30帧之后就会出这个提示。
1 | /** |
回调的逻辑在doCallback()中。
1 | /** |
INPUT调用栈
有用户input交互后,从method trace可以看到,doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos)内部执行的是ConsumeBatchedInputRunnable。最终会走到DecorView.dispatchTouchEvent(),后续就是一系列touch event的事件分发逻辑。
ConsumeBatchedInputRunnable相关代码:
1 | android.view.ViewRootImpl.java |
根据method trace跟一下代码,最后会走到View.dispatchPointerEvent
1 | android.view.ViewRootImpl.java |
ANIMATION调用栈
如果使用ValueAnimator执行动画,可以看到下图中的方法调用栈:
动画执行时,Choreographer.doFrame()会响应AnimationHandler内部的Choreographer.FrameCallback.doFrame(),然后执行到ValueAnimator.onAnimationUpdate(),这个方法也就是业务测实现动画逻辑的地方。
执行动画时,Choreographer回调到AnimationHandler$Choreographer.FrameCallback.doFrame():
1 | AnimationHandler.java |
动画完成后,执行Choreographer.scheduleVsyncLocked()调度下一个Vsync(“调度Vsync”部分会再讲到),再次执行下一帧的动画流程。就这样循环,完成整个动画过程。使用View.postOnAnimation()流程也大致相同。可以借助profiler跟下代码。
那AnimationHandler中的Choreographer.FrameCallback是怎么注册到Choreographer中的呢?
答案是调用ValueAnimator.start()开始动画的时候。start之后会走到ValueAnimator.addAnimationCallback()
1 | android.animation.ValueAnimator.java |
然后执行到AnimationHandler.addAnimationFrameCallback()
1 | android.animation.AnimationHandler.java |
其中,getProvider().postFrameCallback()中的mFrameCallback就是Choreographer回调AnimationHandler的FrameCallback。postFrameCallback()会把该callback会放到Choreographer的mCallbackQueues,当Choreographer.doFrame()执行doCallback()时就从Queue中找到该FrameCallback进行回调
1 | android.view.Choreographer.java |
其他的动画方式也可以通过查看AS->profiler->CPU来看具体的方法调用栈,理解其实现思路。
TRAVERSAL调用栈
traversal过程主要是指View的measure、layout、draw。
像View.invalidate()、View.requestLayout()都会触发traversal流程。跟踪代码,会执行到下面:
1 | android.view.ViewRootImpl.java |
调度Traversal时,为了优先处理UI绘制操作,在消息队列中加入了同步屏障,且Traversal对应的Message设置为异步消息。这块知识点可以参考:Android 同步屏障机制(Sync Barrier)/#more](https://hningoba.github.io/2019/12/06/Android 同步屏障机制(Sync Barrier)/#more))
mChoreographer.postCallback()会执行到下面方法:
1 | android.view.Choreographer.java |
上面得了流程会向底层调度Vsync信号。等下一个Vsync信号到来时,会执行doFrame()。进而在Choreographer.doCallbacks(Choreographer.CALLBACK_TRAVERSAL)中,从CallbackQueue中找到Traversal对应的CallbackRecord执行。
从下图可以看到Choreographer.doCallbacks(Choreographer.CALLBACK_TRAVERSAL)内部执行的是ViewRootImpl.TravasalRunnable。这一点上面也提到了。
TravasalRunnable.performTraversals()内部执行了大家比较熟悉的View绘制流程,即measure、layout、draw。
1 | android.view.ViewRootImpl.java |
调度Vsync
上面讲Animation调用栈时提到每一帧动画结束后都向底层主动调度下一个Vsync,用来做下一帧的渲染工作。具体调度代码在下面:
1 | android.view.Choreographer.java |
最后通过DisplayEventReceiver向JNI申请Vsync信号。
1 | android.view.DisplayEventReceiver.java |
JNI层调度Vsync方法:
1 | android_view_DisplayEventReceiver.cpp |
NativeDisplayEventDispatcher继承自DisplayEventDispatcher,scheduleVsync()本质上是调用DisplayEventDispatcher:
1 | DisplayEventDispatcher.cpp |
当下一个Vsync信号到来时,JNI会回调DisplayEventReceiver.dispatchVsync(),然后执行FrameDisplayEventReceiver.onVsync(),这部分内容上面已经讲到。
1 | android.view.DisplayEventReceiver.java |
帧率计算
方式一:FrameCallback
有些APM计算帧率的方式和处理动画的逻辑类似,利用FrameCallback的回调,在doFrame()中计算帧率。
1 | // 注册自定义FrameCallback |
开始和结束监听帧率时记录start和end时间戳,结束时通过FrameCallback中的帧数就可以计算帧率。
方式二:Looper & Printer
也有工具使用消息机制Looper类中监听日志打印的逻辑来计算主线程卡顿问题,比如BlockCanary。利用该方法不仅可以计算每一帧的耗时情况,也可以计算一定时间内的帧数,从而计算帧率。
看下关键方法:
1 | android.os.Looper.java |
主线程每一帧执行前后,Looper通过一个Printer对象分别打印日志”>>>>> Dispatching to…”和”<<<<< Finished to”。通过Looper.setMessageLogging()可以设置我们自定义的Printer。下面示例代码可以通过监听Looper的日志实现:
1 | public class BlockPrinter implements Printer { |
通过这种方式可以计算每一帧的耗时和帧数。再像方式一一样,开始和结束时记录start和end时间戳,结束时通过预先记录好的帧数就可以计算帧率。
总结
Choreographer收到显示子系统发送的定时脉冲(Vsync)后,协调下一帧的animations、input、drawing工作。
收到Vsync后会响应Choreographer.doFrame(),每一帧的INPUT、ANIMATION、TRAVERSAL都是从这个方法开始执行的。
Choreographer是线程私有的,每个Looper有自己的Choreographer实例,跨线程调度任务通过post callback方式。
计算帧率可以借鉴动画的实现思路,自定义FrameCallback。
- App没有更新操作,比如没有需要执行的动画、用户没有交互等,app层是不会渲染的。因为每一帧的操作都需要先向SurfaceFlinger调度Vsync。比如View.invalidate()、View.requestLayout()、View.postOnAnimation()、ValueAnimator.start()等都会触发Vsync调度。